在很多第一人稱或第三人稱射擊遊戲的單人模式中,玩家的樂趣往往來自於和各式各样的AI敵人戰鬥,而戰鬥的爆發很多時候是因為這些AI在“看見”玩家後就會立即做出反應,比如攻擊、呼叫同伴、躲藏或者逃跑等。
所以這些AI到底是如何偵測,或者說”看到“玩家位置的?
玩家站在敵人的角度来探測目標,它通過向正前方一定扇形區域發射一堆射線來探測目標的位置,如下圖:
這種方法雖然實作起來比較簡單,但它主要有兩個問題:
1.同一時間内發射大量的射線,對遊戲本身的優化來說很不好,容易造成Lag。
2.如果要探測的物體比較小,甚至比兩條射線之間的間隔還要小,那麼射線是無法探測到這個物體的。
於是乎有了另外一種解決這個問題的想法
可以基於探測者自身創造一個球體來探測周圍的物體。
因此我們可以用Unity自帶的Sphere感測器或者Physics裡的OverLaps來構建一個探測區域,如下圖:
而我用的法是Overlaps,程式碼如下:
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour {
public float moveSpeed; //移動速度
public float EyeViewDistance; //視野距離
public float viewAngle = 120f; //視野角度
private Rigidbody rb;
private Collider[] SpottedEnemies; //附近的敵人
// Use this for initialization
void Start () {
rb = GetComponent<Rigidbody>();
}
private void FixedUpdate()
{
DetectEnemy();
}
// Update is called once per frame
void Update ()
{
//AutoMove();
MoveAndTurn();
Debug.DrawLine(transform.position, transform.forward * 100, Color.red); //红色射線面對方向
}
void AutoMove() //向面對的方向自動移動
{
transform.position += transform.forward * moveSpeed * Time.deltaTime;
}
void MoveAndTurn() //玩家移動
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo = new RaycastHit();
//shoot a ray from cam to mouse position which is only detected by gameobject with "Plane" layer.
Physics.Raycast(ray, out hitInfo, 100, LayerMask.GetMask("Plane"));
if (hitInfo.collider != null)
{
transform.LookAt(new Vector3(hitInfo.point.x, transform.position.y, hitInfo.point.z));
}
rb.velocity = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized * moveSpeed;
}
void DetectEnemy() //探測敵人
{
//OverlapSphere内的敵人
SpottedEnemies = Physics.OverlapSphere(transform.position, EyeViewDistance, LayerMask.GetMask("Enemy"));
for(int i = 0;i < SpottedEnemies.Length;i++) //檢測每一個敵人是否在視野區中
{
Vector3 EnemyPosition = SpottedEnemies[i].transform.position; //敵人的位置
//Debug.Log(transform.forward + " 面對的方向");
//Debug.Log("夾角為:" + Vector3.Angle(transform.forward, EnemyPosition - transform.position));
Debug.DrawRay(transform.position, EnemyPosition - transform.position, Color.yellow); //玩家位置到敵人位置的向量
if (Vector3.Angle(transform.forward, EnemyPosition - transform.position) <= viewAngle/2) //敵人是否在視野内
{
//如果在視野内
RaycastHit info = new RaycastHit();
int layermask = LayerMask.GetMask("Enemy", "Obstacles"); //指定射線碰撞的對象
Physics.Raycast(transform.position, EnemyPosition - transform.position, out info,EyeViewDistance,layermask); //向敵人發射射線
Debug.Log(info.collider.gameObject.name);
if(info.collider == SpottedEnemies[i]) //如果途中無其他障礙物,那麼射線就會碰撞敵人
{
DiscoveredEnemy(SpottedEnemies[i]);
}
}
}
}
void DiscoveredEnemy(Collider Enemy) //發現敵人
{
//Do something
Debug.Log("發現敵人:" + Enemy.gameObject.name);
Enemy.GetComponent<Enemy>().BeDiscovered();
}
}
SpottedEnemies是一个Collider組件,我用它來保存這一禎當中處於OverlapSphere形成的球體區域的所有敵人對象,(LayerMask可以讓Overlaps的球體只和指定layer的對象交換)。然後計算玩家面對的方向和探測到的目標方向的夾角Vector3.Angle(transform.forward, EnemyPosition - transform.position)
forward向量代表玩家面朝的方向,v1代表探测到的物体相對於玩家位置的方向,紅色扇形區域代表玩家的視野範圍,那麼計算這兩個向量的夾角,然後判斷下這個夾角是否小於扇形區夾角的一半(即探測目標是否在玩家視野内)就行了。如果夾角小於視野夾角的一半,那麼我們再向目標位置發射一根射線,然後看下射線碰撞到的物體是否是目標對象就行,因為如果玩家和目標之間有障礙物的話,那麼射線是會被障礙物檔下來的(也就是說玩家的"視野"被障礙物"擋住"了)。